/**
 * Copyright (c) 2006-2012, JGraph Ltd
 */
package com.neuronbit.businessflow.intellij.editor.swing;

import com.intellij.openapi.actionSystem.DataKey;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.command.undo.UndoManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.event.DocumentEvent;
import com.intellij.openapi.editor.event.DocumentListener;
import com.intellij.openapi.editor.impl.DocumentImpl;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.ReadonlyStatusHandler;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiFile;
import com.intellij.util.Alarm;
import com.intellij.util.ui.UIUtil;
import com.mxgraph.io.mxCodec;
import com.mxgraph.io.mxCodecRegistry;
import com.mxgraph.model.mxCell;
import com.mxgraph.model.mxGeometry;
import com.mxgraph.model.mxICell;
import com.mxgraph.model.mxIGraphModel;
import com.mxgraph.swing.mxGraphComponent;
import com.mxgraph.util.*;
import com.mxgraph.view.mxCellState;
import com.mxgraph.view.mxGraph;
import com.neuronbit.businessflow.intellij.editor.DesignerToolWindowManager;
import com.neuronbit.businessflow.intellij.editor.palette.impl.PaletteToolWindowManager;
import com.neuronbit.businessflow.intellij.editor.UIDesignerBundle;
import com.neuronbit.businessflow.intellij.editor.codec.ParamCodec;
import com.neuronbit.businessflow.intellij.editor.codec.ReturnCodec;
import com.neuronbit.businessflow.intellij.editor.codec.CallCodec;
import com.neuronbit.businessflow.intellij.editor.codec.ExceptionHandlerCodec;
import com.neuronbit.businessflow.intellij.editor.swing.editor.*;
import org.apache.commons.lang3.NotImplementedException;
import org.jetbrains.annotations.NotNull;
import org.w3c.dom.Document;

import javax.swing.event.EventListenerList;
import java.awt.*;
import java.net.URL;
import java.text.NumberFormat;
import java.util.List;
import java.util.*;

import static java.util.Objects.requireNonNull;

/**
 * The type Gui editor.
 */
public class GuiEditor extends BasicGraphEditor  {
    private static final Logger LOG = Logger.getInstance(GuiEditor.class);
    /**
     *
     */
    private static final long serialVersionUID = -4601740824088314699L;

    /**
     * Holds the shared number formatter.
     *
     * @see NumberFormat#getInstance() NumberFormat#getInstance()NumberFormat#getInstance()
     */
    public static final NumberFormat numberFormat = NumberFormat.getInstance();


    /**
     * Holds the URL for the icon to be used as a handle for creating new
     * connections. This is currently unused.
     */
    public static URL url = null;

    //GraphEditor.class.getResource("/com/mxgraph/editor/swing/images/connector.gif");

    /**
     * The constant DATA_KEY.
     */
    public static final DataKey<GuiEditor> DATA_KEY = DataKey.create(GuiEditor.class.getName());
    private final DocumentListener myDocumentListener;
    private final EventListenerList myListenerList = new EventListenerList();
    private final com.intellij.openapi.editor.Document myDocument;
    private Object myNextSaveGroupId = new Object();
    /**
     * GuiEditor should not react on own events. If {@code myInsideChange}
     * is {@code true} then we do not react on incoming DocumentEvent.
     */
    private boolean myInsideChange;

    static {
        mxCodecRegistry.register(new CallCodec());
        mxCodecRegistry.register(new ParamCodec());
        mxCodecRegistry.register(new ReturnCodec());
        mxCodecRegistry.register(new ExceptionHandlerCodec());
    }

    /**
     * Instantiates a new Gui editor.
     *
     * @param project the project
     * @param module  the module
     * @param file    the file
     */
    public GuiEditor(@NotNull Project project, Module module, VirtualFile file) {
        this(project, module, file, "mxGraph Editor", new CustomGraphComponent(project, module, new CustomGraph()));
    }

    /**
     * Instantiates a new Gui editor.
     *
     * @param project   the project
     * @param module    the module
     * @param file      the file
     * @param appTitle  the app title
     * @param component the component
     */
    public GuiEditor(@NotNull Project project, Module module, VirtualFile file, String appTitle, mxGraphComponent component) {
        super(project, module, file, appTitle, component);
        graphComponent.setConnectable(false);

        final mxGraph graph = graphComponent.getGraph();

//        PropertyInspector inspector = insertBasicProperty(mxResources.get("basic"));
//        inspector.setEditor(this);
        //JPanel stylePanel = insertStyleProperty(mxResources.get("style"));
        //JPanel otherPanel = insertProperty(mxResources.get("other"));

        // We need to synchronize GUI editor with the document
        final Alarm alarm = new Alarm();
        myDocumentListener = new DocumentListener() {
            @Override
            public void documentChanged(@NotNull final DocumentEvent e) {
                if (!myInsideChange) {
                    UndoManager undoManager = UndoManager.getInstance(getProject());
                    alarm.cancelAllRequests();
                    alarm.addRequest(new MySynchronizeRequest(undoManager.isUndoOrRedoInProgress()),
                            100/*any arbitrary delay*/, ModalityState.stateForComponent(GuiEditor.this));
                }
            }
        };

        // Prepare document
        myDocument = FileDocumentManager.getInstance().getDocument(file);
        if (myDocument instanceof DocumentImpl) {
            myDocument.addDocumentListener(myDocumentListener);
            ((DocumentImpl) myDocument).setAcceptSlashR(true);
        }

        //Read from file
        readFromFile(false);
        refreshPages();

        mxEventSource.mxIEventListener mxIEventListener = (sender, evt) -> saveToFile();
        graph.getModel().addListener(mxEvent.CHANGE, mxIEventListener);
        graph.getModel().addListener(mxEvent.CHANGE, (sender, evt) -> {
            refreshPages();
        });

        if (!ApplicationManager.getApplication().isUnitTestMode()) {
            UIUtil.invokeLaterIfNeeded(() -> {
                DesignerToolWindowManager.getInstance(project).bind(this);
                PaletteToolWindowManager.getInstance(project).bind(this);
            });
        }
    }

    private void refreshPages() {
        boolean pageCountChanged = ((CustomGraphComponent) graphComponent).adjustPageCount();
        if (pageCountChanged) {
            refreshGraphComponent();
        }
        boolean hasBlankPages = ((CustomGraphComponent) graphComponent).removeBlankPages();
        if (hasBlankPages) {
            refreshGraphComponent();
        }
    }

    private void refreshGraphComponent() {
        Object[] selectionCells = graphComponent.getGraph().getSelectionCells();
        graphComponent.getGraph().clearSelection();

        graphComponent.getGraphControl().updatePreferredSize();
        graphComponent.getGraphControl().revalidate();
        graphComponent.getGraphControl().repaint();

        graphComponent.revalidate();
        graphComponent.refresh();

        graphComponent.getGraph().addSelectionCells(selectionCells);
    }

    /**
     * Adds specified hierarchy change listener
     *
     * @param l the l
     */
    public void addHierarchyChangeListener(@NotNull final HierarchyChangeListener l) {
        myListenerList.add(HierarchyChangeListener.class, l);
    }

    /**
     * Removes specified hierarchy change listener
     *
     * @param l the l
     */
    public void removeHierarchyChangeListener(@NotNull final HierarchyChangeListener l) {
        myListenerList.remove(HierarchyChangeListener.class, l);
    }


    /**
     * Ensure editable boolean.
     *
     * @return the boolean
     */
    public boolean ensureEditable() {
        if (isEditable()) {
            return true;
        }
        VirtualFile sourceFileToCheckOut = null;
//        if (!GuiDesignerConfiguration.getInstance(getProject()).INSTRUMENT_CLASSES) {
//            final String classToBind = myRootContainer.getClassToBind();
//            if (classToBind != null && !classToBind.isEmpty()) {
//                PsiClass psiClass = FormEditingUtil.findClassToBind(getModule(), classToBind);
//                if (psiClass != null) {
//                    sourceFileToCheckOut = psiClass.getContainingFile().getVirtualFile();
//                }
//            }
//        }

        final ReadonlyStatusHandler.OperationStatus status;
        if (sourceFileToCheckOut != null) {
            status = ReadonlyStatusHandler.getInstance(getProject()).ensureFilesWritable(Arrays.asList(myFile, sourceFileToCheckOut));
        } else {
            status = ReadonlyStatusHandler.getInstance(getProject()).ensureFilesWritable(Collections.singletonList(myFile));
        }
        return !status.hasReadonlyFiles();
    }

    /**
     * Is editable boolean.
     *
     * @return the boolean
     */
    public boolean isEditable() {
        final com.intellij.openapi.editor.Document document = FileDocumentManager.getInstance().getDocument(myFile);
        return document != null && document.isWritable();
    }

    /**
     * Refresh and save.
     *
     * @param forceSync the force sync
     */
    public void refreshAndSave(final boolean forceSync) {
        //TODO shunyun 2024/3/13:
        //throw new NotImplementedException();
//        DesignerToolWindow toolWindow = DesignerToolWindowManager.getInstance(this);
//        if (toolWindow == null) {
//            return;
//        }
//
//        // Update property inspector
//        final PropertyInspector propertyInspector = toolWindow.getPropertyInspector();
//        if (propertyInspector != null) {
//            propertyInspector.synchWithTree(forceSync);
//        }

//        refresh();
        saveToFile();
//        // TODO[yole]: install appropriate listeners so that the captions repaint themselves at correct time
//        myHorzCaptionPanel.repaint();
//        myVertCaptionPanel.repaint();
    }

    private void saveToFile() {
        LOG.debug("GuiEditor.saveToFile(): group ID=" + myNextSaveGroupId);
        CommandProcessor.getInstance().executeCommand(getProject(), () -> ApplicationManager.getApplication().runWriteAction(() -> {
            myInsideChange = true;
            try {
                mxCodec codec = new mxCodec();
                final String newText = mxXmlUtils.getXml(codec.encode(getGraphComponent().getGraph().getModel()));
                // TODO shunyun 2024/3/15:Model和RadComponent看起来有点冗余，但是又不一样，是否需要整合呢？
//                final XmlWriter writer = new XmlWriter();
//                getRootContainer().write(writer);
//                final String newText = writer.getText();
                final String oldText = myDocument.getText();

                try {
                    final ReplaceInfo replaceInfo = findFragmentToChange(oldText, newText);
                    if (replaceInfo.getStartOffset() == -1) {
                        // do nothing - texts are equal
                    } else {
                        myDocument.replaceString(replaceInfo.getStartOffset(), replaceInfo.getEndOffset(), replaceInfo.getReplacement());
                    }
                } catch (Exception e) {
                    LOG.error(e);
                    myDocument.replaceString(0, oldText.length(), newText);
                }
            } finally {
                myInsideChange = false;
            }
        }), UIDesignerBundle.message("gui.editor.save.command"), myNextSaveGroupId);
        myNextSaveGroupId = new Object();

        fireHierarchyChanged();
    }

    private void fireHierarchyChanged() {
        final HierarchyChangeListener[] listeners = myListenerList.getListeners(HierarchyChangeListener.class);
        for (final HierarchyChangeListener listener : listeners) {
            listener.hierarchyChanged();
        }
    }

    /**
     * Find fragment to change replace info.
     *
     * @param oldText the old text
     * @param newText the new text
     * @return the replace info
     */
    public static ReplaceInfo findFragmentToChange(final String oldText, final String newText) {
        if (oldText.equals(newText)) {
            return new ReplaceInfo(-1, -1, null);
        }

        final int oldLength = oldText.length();
        final int newLength = newText.length();

        int startOffset = 0;
        while (
                startOffset < oldLength && startOffset < newLength &&
                        oldText.charAt(startOffset) == newText.charAt(startOffset)
        ) {
            startOffset++;
        }

        int endOffset = oldLength;
        while (true) {
            if (endOffset <= startOffset) {
                break;
            }
            final int idxInNew = newLength - (oldLength - endOffset) - 1;
            if (idxInNew < startOffset) {
                break;
            }

            final char c1 = oldText.charAt(endOffset - 1);
            final char c2 = newText.charAt(idxInNew);
            if (c1 != c2) {
                break;
            }
            endOffset--;
        }

        return new ReplaceInfo(startOffset, endOffset, newText.substring(startOffset, newLength - (oldLength - endOffset)));
    }

    /**
     * Is undo redo in progress boolean.
     *
     * @return the boolean
     */
    public boolean isUndoRedoInProgress() {
        return UndoManager.getInstance(getProject()).isUndoOrRedoInProgress();
    }

    /**
     * Gets root container.
     *
     * @return the root container
     */
    public RadRootContainer getRootContainer() {
        return null;
    }

    /**
     * Gets psi file.
     *
     * @return the psi file
     */
    public PsiFile getPsiFile() {
        throw new NotImplementedException();
    }


    /**
     * Read from file.
     *
     * @param keepSelection the keep selection
     */
    public void readFromFile(final boolean keepSelection) {
        String[] selection = null;
        if (keepSelection) {
            selection = SelectionState.getSelection(this);
//                tabbedPaneSelectedTabs = saveTabbedPaneSelectedTabs();
        }

        final String text = myDocument.getText();
        Document document = mxXmlUtils.parseXml(text);

        mxCodec codec = new mxCodec(document);
        if (document != null) {
            codec.decode(document.getDocumentElement(), getGraphComponent().getGraph().getModel());
        }

        if (!keepSelection) {
            reset();
        } else {
            SelectionState.restoreSelection(this, selection);
//                restoreTabbedPaneSelectedTabs(tabbedPaneSelectedTabs);
        }

        DumbService.getInstance(getProject()).smartInvokeLater(() -> fireHierarchyChanged());
//        try {
//            ComponentPtr[] selection = null;
//            Map<String, String> tabbedPaneSelectedTabs = null;
//            if (keepSelection) {
//                selection = SelectionState.getSelection(this);
//                tabbedPaneSelectedTabs = saveTabbedPaneSelectedTabs();
//            }
//            Locale oldLocale = null;
//            if (myRootContainer != null) {
//                oldLocale = myRootContainer.getStringDescriptorLocale();
//            }
//
//            final String text = myDocument.getText();
//
//            final ClassLoader classLoader = LoaderFactory.getInstance(getProject()).getLoader(myFile);
//
//            final LwRootContainer rootContainer = Utils.getRootContainer(text, new CompiledClassPropertiesProvider(classLoader));
//            final RadRootContainer container = XmlReader.createRoot(this, rootContainer, classLoader, oldLocale);
//            setRootContainer(container);
//            if (keepSelection) {
//                SelectionState.restoreSelection(this, selection);
//                restoreTabbedPaneSelectedTabs(tabbedPaneSelectedTabs);
//            }
//            myInvalid = false;
//            myCardLayout.show(myCardPanel, CARD_VALID);
//            refresh();
//        }
//        catch (Exception exc) {
//            Throwable original = exc;
//            while (original instanceof InvocationTargetException) {
//                original = original.getCause();
//            }
//            showInvalidCard(original);
//        }
//        catch (final LinkageError exc) {
//            showInvalidCard(exc);
//        }
    }

    /**
     * The type Custom graph component.
     */
    public static class CustomGraphComponent extends mxGraphComponent {

        /**
         *
         */
        private static final long serialVersionUID = -6833603133512882012L;

        /**
         * Instantiates a new Custom graph component.
         *
         * @param project the project
         * @param module  the module
         * @param graph   the graph
         */
        public CustomGraphComponent(@NotNull Project project, Module module, mxGraph graph) {
            super(project, module, graph);

            // Sets switches typically used in an module
            setPageVisible(true);
            setGridVisible(true);
            setToolTips(true);
            getConnectionHandler().setCreateTarget(true);

            // Loads the defalt stylesheet from an external file
            mxCodec codec = new mxCodec();
            Document doc = mxUtils.loadDocument(requireNonNull(GuiEditor.class.getResource(
                    "/com/mxgraph/editor/swing/resources/default-style.xml"))
                    .toString());
            codec.decode(requireNonNull(doc).getDocumentElement(), graph.getStylesheet());

            // Sets the background to white
            getViewport().setOpaque(true);
            getViewport().setBackground(Color.WHITE);
        }

        /**
         * Overrides drop behaviour to set the cell style if the target
         * is not a valid drop target and the cells are of the same
         * type (eg. both vertices or both edges).
         */
        public Object[] importCells(Object[] cells, double dx, double dy,
                                    Object target, Point location) {
            if (target == null && cells.length == 1 && location != null) {
                target = getCellAt(location.x, location.y);

                if (target instanceof mxICell && cells[0] instanceof mxICell) {
                    mxICell targetCell = (mxICell) target;
                    mxICell dropCell = (mxICell) cells[0];

                    if (targetCell.isVertex() == dropCell.isVertex()
                            || targetCell.isEdge() == dropCell.isEdge()) {
                        mxIGraphModel model = graph.getModel();
                        model.setStyle(target, model.getStyle(cells[0]));
                        graph.setSelectionCell(target);

                        return null;
                    }
                }
            }

            return super.importCells(cells, dx, dy, target, location);
        }


        /**
         * Adjust page count boolean.
         *
         * @return the boolean
         */
        public boolean adjustPageCount() {
            mxPoint translate = graph.getView().getTranslate();
            double viewScale = graph.getView().getScale();

            int x0 = (int) Math.round(translate.getX() * viewScale) - 1;
            int y0 = (int) Math.round(translate.getY() * viewScale) - 1;

            Dimension d = getPreferredSizeForPage();
            int w = (int) Math.round(d.width * viewScale) + 2;
            int h = (int) Math.round(d.height * viewScale) + 2;

            Rectangle pageRect = new Rectangle(x0, y0, w, h);

            mxRectangle graphBounds = graph.getGraphBounds();
            Rectangle graphRect = graphBounds.getRectangle();

            int pageWidth = w / horizontalPageCount;
            int pageHeight = h / verticalPageCount;

            if (!pageRect.contains(graphRect)) {
                int hLeftPageGap = 0;
                int hRightPageGap = 0;
                if (pageRect.getMaxX() < graphRect.getMaxX()) {
                    hRightPageGap += (int) (Math.round(Math.abs(pageRect.getMaxX() - graphRect.getMaxX()) / pageWidth) + 1);
                }
                if (pageRect.getX() > graphRect.getX()) {
                    hLeftPageGap += (int) (Math.round(Math.abs(pageRect.getX() - graphRect.getX()) / pageWidth) + 1);
                }
                setHorizontalPageCount(getHorizontalPageCount() + hLeftPageGap + hRightPageGap);

                if (hLeftPageGap > 0) {
                    double v = (hLeftPageGap * pageWidth) / viewScale;
                    Object[] cells = graph.getChildCells(graph.getDefaultParent());
                    graph.moveCells(cells, v, 0);
                }

                int vTopPageGap = 0;
                int vBottomPageGap = 0;

                if (pageRect.getMaxY() < graphRect.getMaxY()) {
                    vBottomPageGap = (int) (Math.round(Math.abs(pageRect.getMaxY() - graphRect.getMaxY()) / pageHeight) + 1);
                }

                if (pageRect.getY() > graphRect.getY()) {
                    vTopPageGap = (int) (Math.round(Math.abs(pageRect.getY() - graphRect.getY()) / pageHeight) + 1);
                }
                setVerticalPageCount(getVerticalPageCount() + vTopPageGap + vBottomPageGap);

                if (vTopPageGap > 0) {
                    double v = (vTopPageGap * pageHeight) / viewScale;
                    Object[] cells = graph.getChildCells(graph.getDefaultParent());
                    graph.moveCells(cells, 0, v);
                }
                return true;
            }

            return false;
        }

        /**
         * Remove blank pages boolean.
         *
         * @return the boolean
         */
        public boolean removeBlankPages() {
            mxPoint translate = graph.getView().getTranslate();
            double viewScale = graph.getView().getScale();

            int x0 = (int) Math.round(translate.getX() * viewScale) - 1;
            int y0 = (int) Math.round(translate.getY() * viewScale) - 1;

            Dimension d = getPreferredSizeForPage();
            int w = (int) Math.round(d.width * viewScale) + 2;
            int h = (int) Math.round(d.height * viewScale) + 2;

            mxRectangle graphBounds = graph.getGraphBounds();
            Rectangle graphRect = graphBounds.getRectangle();

            int pageWidth = w / horizontalPageCount;
            int pageHeight = h / verticalPageCount;

            List<mxPage> pages = new ArrayList<>();
            for (int i = 0; i < horizontalPageCount; i++) {
                int dx = i * w / horizontalPageCount;
                for (int j = 0; j < verticalPageCount; j++) {
                    int dy = j * h / verticalPageCount;
                    pages.add(new mxPage(i, j, new Rectangle(x0 + dx, y0 + dy, pageWidth, pageHeight)));
                }
            }

            mxPage p0 = null;
            List<mxPage> shouldRemovePages = new ArrayList<>(pages.size());
            int minX = Integer.MAX_VALUE;
            int maxX = 0;

            int minY = Integer.MAX_VALUE;
            int maxY = 0;
            for (mxPage page : pages) {
                Rectangle rectangle = page.getRectangle();
                if (rectangle.intersects(graphRect)) {
                    if (minX > page.getI()) {
                        minX = page.getI();
                    }
                    if (maxX < page.getI()) {
                        maxX = page.getI();
                    }
                    if (minY > page.getJ()) {
                        minY = page.getJ();
                    }
                    if (maxY < page.getJ()) {
                        maxY = page.getJ();
                    }
                    if (rectangle.contains(graphRect.getX(), graphRect.getY())) {
                        p0 = page;
                    }
                } else {
                    shouldRemovePages.add(page);
                }
            }

            if (p0 == null) {
                Collections.sort(pages);
                for (mxPage page : pages) {
                    if (!shouldRemovePages.contains(page)) {
                        p0 = page;
                        break;
                    }
                }
            }

            if (p0 == null) {
                LOG.error("p0 is null");
                return false;
            }

            int hPageCount = maxX - minX + 1;
            int vPageCount = maxY - minY + 1;

            if (hPageCount != getHorizontalPageCount()) {
                setHorizontalPageCount(hPageCount);
            }
            if (vPageCount != getVerticalPageCount()) {
                setVerticalPageCount(vPageCount);
            }

            for (mxPage shouldRemovePage : shouldRemovePages) {
                if (shouldRemovePage.getI() < p0.getI()) {
                    double v = ((Math.round(Math.abs(x0 - graphRect.getX()) / pageWidth)) * pageWidth) / viewScale;
                    Object[] cells = graph.getChildCells(graph.getDefaultParent());
                    graph.moveCells(cells, -v, 0);
                }
                if (shouldRemovePage.getJ() < p0.getJ()) {
                    double v = ((Math.round(Math.abs(y0 - graphRect.getY()) / pageHeight)) * pageHeight) / viewScale;
                    Object[] cells = graph.getChildCells(graph.getDefaultParent());
                    graph.moveCells(cells, 0, -v);
                }
                return true;
            }
            return false;
        }
    }

    /**
     * A graph that creates new edges from a given template edge.
     */
    public static class CustomGraph extends mxGraph {
        /**
         * Holds the edge to be used as a template for inserting new edges.
         */
        protected Object edgeTemplate;

        /**
         * Custom graph that defines the alternate edge style to be used when
         * the middle control point of edges is double clicked (flipped).
         */
        public CustomGraph() {
            setAlternateEdgeStyle("edgeStyle=mxEdgeStyle.ElbowConnector;elbow=vertical");
        }

        /**
         * Sets the edge template to be used to inserting edges.
         *
         * @param template the template
         */
        public void setEdgeTemplate(Object template) {
            edgeTemplate = template;
        }

        /**
         * Prints out some useful information about the cell in the tooltip.
         */
        public String getToolTipForCell(Object cell) {
            String tip = "<html>";
            mxGeometry geo = getModel().getGeometry(cell);
            mxCellState state = getView().getState(cell);

            if (getModel().isEdge(cell)) {
                tip += "points={";

                if (geo != null) {
                    List<mxPoint> points = geo.getPoints();

                    if (points != null) {
                        Iterator<mxPoint> it = points.iterator();

                        while (it.hasNext()) {
                            mxPoint point = it.next();
                            tip += "[x=" + numberFormat.format(point.getX())
                                    + ",y=" + numberFormat.format(point.getY())
                                    + "],";
                        }

                        tip = tip.substring(0, tip.length() - 1);
                    }
                }

                tip += "}<br>";
                tip += "absPoints={";

                if (state != null) {

                    for (int i = 0; i < state.getAbsolutePointCount(); i++) {
                        mxPoint point = state.getAbsolutePoint(i);
                        tip += "[x=" + numberFormat.format(point.getX())
                                + ",y=" + numberFormat.format(point.getY())
                                + "],";
                    }

                    tip = tip.substring(0, tip.length() - 1);
                }

                tip += "}";
            } else {
                tip += "geo=[";

                if (geo != null) {
                    tip += "x=" + numberFormat.format(geo.getX()) + ",y="
                            + numberFormat.format(geo.getY()) + ",width="
                            + numberFormat.format(geo.getWidth()) + ",height="
                            + numberFormat.format(geo.getHeight());
                }

                tip += "]<br>";
                tip += "state=[";

                if (state != null) {
                    tip += "x=" + numberFormat.format(state.getX()) + ",y="
                            + numberFormat.format(state.getY()) + ",width="
                            + numberFormat.format(state.getWidth())
                            + ",height="
                            + numberFormat.format(state.getHeight());
                }

                tip += "]";
            }

            mxPoint trans = getView().getTranslate();

            tip += "<br>scale=" + numberFormat.format(getView().getScale())
                    + ", translate=[x=" + numberFormat.format(trans.getX())
                    + ",y=" + numberFormat.format(trans.getY()) + "]";
            tip += "</html>";

            return tip;
        }

        /**
         * Overrides the method to use the currently selected edge template for
         * new edges.
         *
         * @param parent
         * @param id
         * @param value
         * @param source
         * @param target
         * @param style
         * @return
         */
        public Object createEdge(Object parent, String id, Object value,
                                 Object source, Object target, String style) {
            if (edgeTemplate != null) {
                mxCell edge = (mxCell) cloneCells(new Object[]{edgeTemplate})[0];
                edge.setId(id);

                return edge;
            }

            return super.createEdge(parent, id, value, source, target, style);
        }

    }


    /**
     * The type Replace info.
     */
    public static final class ReplaceInfo {
        private final int myStartOffset;
        private final int myEndOffset;
        private final String myReplacement;

        /**
         * Instantiates a new Replace info.
         *
         * @param startOffset the start offset
         * @param endOffset   the end offset
         * @param replacement the replacement
         */
        public ReplaceInfo(final int startOffset, final int endOffset, final String replacement) {
            myStartOffset = startOffset;
            myEndOffset = endOffset;
            myReplacement = replacement;
        }

        /**
         * Gets start offset.
         *
         * @return the start offset
         */
        public int getStartOffset() {
            return myStartOffset;
        }

        /**
         * Gets end offset.
         *
         * @return the end offset
         */
        public int getEndOffset() {
            return myEndOffset;
        }

        /**
         * Gets replacement.
         *
         * @return the replacement
         */
        public String getReplacement() {
            return myReplacement;
        }
    }

    private class MySynchronizeRequest implements Runnable {
        private final boolean myKeepSelection;

        /**
         * Instantiates a new My synchronize request.
         *
         * @param keepSelection the keep selection
         */
        MySynchronizeRequest(final boolean keepSelection) {
            myKeepSelection = keepSelection;
        }

        @Override
        public void run() {
            if (getModule().isDisposed()) {
                return;
            }
            Project project = getProject();
            if (project.isDisposed()) {
                return;
            }
            LOG.debug("Synchronizing GUI editor " + myFile.getName() + " to document");
            PsiDocumentManager.getInstance(project).commitDocument(myDocument);
            readFromFile(myKeepSelection);
        }
    }

//	/**
//	 *
//	 * @param args
//	 */
//	public static void main(String[] args)
//	{
//		try
//		{
//			UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
//		}
//		catch (Exception e1)
//		{
//			e1.printStackTrace();
//		}
//
//		mxSwingConstants.SHADOW_COLOR = Color.LIGHT_GRAY;
//		mxConstants.W3C_SHADOWCOLOR = "#D3D3D3";
//
//		GraphEditor editor = new GraphEditor();
//		editor.createFrame(new EditorMenuBar(editor)).setVisible(true);
//	}
}
